﻿using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace WPFLoaders
{
    public class LinearLoader : UserControl
    {
        public static int DefaultNumberOfElements = 36;

        public static double DefaultAnimationOffsetScale = 1.0;

        public static readonly DependencyProperty NumberOfElementsProperty =
            DependencyProperty.RegisterAttached("NumberOfElements", typeof(int), typeof(LinearLoader), new PropertyMetadata(DefaultNumberOfElements, OnChange));

        public int NumberOfElements
        {
            get { return (int)GetValue(NumberOfElementsProperty); }
            set { SetValue(NumberOfElementsProperty, value); }
        }

        public static readonly DependencyProperty ElementTemplateProperty =
            DependencyProperty.RegisterAttached("ElementTemplate", typeof(DataTemplate), typeof(LinearLoader), new PropertyMetadata(default(DataTemplate), OnChange));

        public DataTemplate ElementTemplate
        {
            get { return (DataTemplate) GetValue(ElementTemplateProperty); }
            set { SetValue(ElementTemplateProperty, value); }
        }

        public static readonly DependencyProperty AnimationProperty =
            DependencyProperty.RegisterAttached("Animation", typeof(Storyboard), typeof(LinearLoader), new PropertyMetadata(default(Storyboard), OnChange));

        public Storyboard Animation
        {
            get { return (Storyboard) GetValue(AnimationProperty); }
            set { SetValue(AnimationProperty, value); }
        }

        public static readonly DependencyProperty AnimationOffsetScaleProperty =
            DependencyProperty.RegisterAttached("AnimationOffsetScale", typeof(double), typeof(LinearLoader), new PropertyMetadata(DefaultAnimationOffsetScale, OnChange));

        /// <summary>Describes to what ratio the animation should be offset</summary>
        /// <remarks>
        /// An animation offset of zero would make all animations start at the same point while 0.5 would offset the animation from start again halfway through.
        /// Anything over 1.0 would make the animation offset below the animation duration makin the last item not offset to before the animation end.
        /// </remarks>
        public double AnimationOffsetScale
        {
            get { return (double)GetValue(AnimationOffsetScaleProperty); }
            set { SetValue(AnimationOffsetScaleProperty, value); }
        }

        public static readonly DependencyProperty ElementDistanceProperty =
            DependencyProperty.Register("ElementDistance", typeof (Point), typeof (LinearLoader), new PropertyMetadata(default(Point)));

        public Point ElementDistance
        {
            get { return (Point) GetValue(ElementDistanceProperty); }
            set { SetValue(ElementDistanceProperty, value); }
        }

        private readonly Border _border;
        private readonly Canvas _canvas;

        public LinearLoader()
        {
            _border = new Border();
            Content = _border;

            _canvas = new Canvas
            {
                Width = 0,
                Height = 0,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };

            _border.Child = _canvas;
        }

        private static void OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((LinearLoader)d).RebuildLoader();
        }

        public void RebuildLoader()
        {
            int objects = NumberOfElements;

            var center = new Point(ElementDistance.X * NumberOfElements * 0.5, ElementDistance.Y * NumberOfElements * 0.5);
            try
            {
                _canvas.Children.Clear();

                for (int i = 0; i < objects; ++i)
                {
                    var element = new ContentPresenter
                    {
                        ContentTemplate = ElementTemplate,
                        Content = (i / (double)objects)
                    };

                    element.SetValue(Canvas.LeftProperty, i * ElementDistance.X - center.X);
                    element.SetValue(Canvas.TopProperty, i * ElementDistance.Y - center.Y);
                    
                    _canvas.Children.Add(element);
                    
                    AnimateAtOffset(element, (double) i/objects);
                }
            }
            catch (Exception exc)
            {
                throw new Exception("Failed to construct loader: " + exc.Message, exc);
            }
        }

        private void AnimateAtOffset(FrameworkElement element, double animationOffset)
        {
            if (Animation == null)
                return;

            if (AnimationNotOffset)
            {
                Animation.Begin(element);
                return;
            }

            animationOffset = animationOffset * AnimationOffsetScale;
            var animationLength = Animation.Duration.TimeSpan.TotalSeconds;
            var offsetSeconds = animationOffset*animationLength;
            while (offsetSeconds < 0)
                offsetSeconds += Animation.Duration.TimeSpan.TotalSeconds;

            var offsetTime = TimeSpan.FromSeconds(offsetSeconds);

            Animation.Begin(element, true); // true = can seek/control animation
            Animation.Seek(element, offsetTime, TimeSeekOrigin.BeginTime);
        }

        /// <summary>The animation will not be offset if the scale is zero effectively giving all items the same offset</summary>
        protected bool AnimationNotOffset
        {
            get { return Math.Abs(AnimationOffsetScale - 0) < 0.0001; }
        }
    }
}
